﻿using System;

namespace Phoenix.Diagnostics
{
    public static class Trace
    {
        static string traceStringFormat;
        static Action traceScopeCallback;
        static readonly object writeLock = new object();

        static Trace()
        {
            Appenders = new TraceAppenderCollection {new SystemTraceListener()};
            Filters = new TraceFilterCollection();
            TraceLevel = TraceLevel.Info;
            IndentSize = 4;
            ClassNameLength = 25;
        }

        public static int IndentLevel { get; internal set; }
        public static int IndentSize { get; set; }

        /// <summary>
        /// Pads the class name up to this size (for standard width formatting)
        /// </summary>
        public static int ClassNameLength { get; set; }

        public static void Indent()
        {
            IndentLevel++;
        }

        public static void Unindent()
        {
            IndentLevel--;
        }

        public static TraceAppenderCollection Appenders { get; private set; }

        /// <summary>
        /// If you want to filter a particular trace source because it is too noisy, just add it to this collection
        /// </summary>
        public static TraceFilterCollection Filters { get; private set; }

        public static TraceLevel TraceLevel { get; set; }

        /// <summary>
        /// {0} for date, {1} for time, {2} for category, {3} for trace level, {4} for source class, {5} for trace message
        /// default: {0} {1} - {2}{3} [{4}] - {5}
        /// example: 1/1/11 1:11 - Debug [SomeClass] - My debug message
        /// Set value to null to reset to default
        /// </summary>
        public static string TraceStringFormat
        {
            get { return traceStringFormat ?? "{0} {1} - {2}{3} [{4}] - {5}"; }
            set { traceStringFormat = value; }
        }

        public static void WriteDebug(object source, Func<string> messageCallback, string category = TraceCategory.None)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            WriteDebug(source.GetType(), messageCallback, category);
        }

        public static void WriteDebug(Type sourceType, Func<string> messageCallback, string category = TraceCategory.None)
        {
            Write(TraceLevel.Debug, sourceType, messageCallback, category);
        }

        public static void WriteInfo(object source, Func<string> messageCallback, string category = TraceCategory.None)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            WriteInfo(source.GetType(), messageCallback, category);
        }

        public static void WriteInfo(Type sourceType, Func<string> messageCallback, string category = TraceCategory.None)
        {
            Write(TraceLevel.Info, sourceType, messageCallback, category);
        }

        public static void WriteWarning(object source, Func<string> messageCallback, string category = TraceCategory.None)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            WriteWarning(source.GetType(), messageCallback, category);
        }

        public static void WriteWarning(Type sourceType, Func<string> messageCallback, string category = TraceCategory.None)
        {
            Write(TraceLevel.Warning, sourceType, messageCallback, category);
        }

        public static void WriteError(object source, Func<string> messageCallback, string category = TraceCategory.None, Exception exception = null)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            WriteError(source.GetType(), messageCallback, category, exception);
        }

        public static void WriteError(Type sourceType, Func<string> messageCallback, string category = TraceCategory.None, Exception exception = null)
        {
            Write(TraceLevel.Error, sourceType, messageCallback, category, exception);
        }

        static void Write(TraceLevel traceLevel, Type sourceType, Func<string> traceMessage, string category = TraceCategory.None, Exception exception = null)
        {
            if (sourceType == null)
                throw new ArgumentNullException("sourceType");

            if (traceMessage == null)
                throw new ArgumentNullException("traceMessage");

            if (!WillTrace(traceLevel, sourceType)) return;

            if (traceScopeCallback != null)
            {
                traceScopeCallback();
                traceScopeCallback = null;
            }

            lock (writeLock)
            {
                var message = traceMessage();
                WriteInternal(DateTime.Now, traceLevel, sourceType, message, category, exception);
            }
        }

        static void WriteInternal(DateTime logTime, TraceLevel traceLevel, Type sourceType, string traceMessage, string category, Exception ex)
        {
            if (ClassNameLength < sourceType.Name.Length)
                ClassNameLength = sourceType.Name.Length;

            if (ex != null)
                traceMessage = string.Format("{0}{1}{2}", traceMessage, Environment.NewLine, ex);
            var formattedMessage = string.Format(TraceStringFormat,
                                                 logTime.ToShortDateString(),
                                                 logTime.ToString("hh:mm:ss.fff"),
                                                 string.IsNullOrEmpty(category) ? string.Empty : string.Concat(category, " "),
                                                 Enum.GetName(typeof(TraceLevel), traceLevel).PadRight(7),
                                                 sourceType.Name.PadRight(ClassNameLength),
                                                 string.Empty.PadLeft(IndentLevel * IndentSize) + traceMessage);

            foreach (var appender in Appenders)
            {
                appender.Write(logTime, traceLevel, category, sourceType, traceMessage, formattedMessage);
            }
        }

        internal static bool WillTrace(TraceLevel whatLevel, Type sourceType)
        {
            return Appenders.Count > 0 && (int)TraceLevel <= (int)whatLevel && !Filters.Contains(sourceType);
        }

        internal static void BeginTraceScope(Action<Action<TraceLevel, Type, string>> directWriteLine)
        {
            if (traceScopeCallback != null)
                traceScopeCallback();

            var time = DateTime.Now;
            traceScopeCallback = () => directWriteLine((l, type, log) => WriteInternal(time, l, type, log, TraceCategory.None, null));
        }

        internal static void ClearTraceScopeCallback()
        {
            traceScopeCallback = null;
        }
    }
}